import { execSync as exec } from "child_process";
import fs from "fs";
import path from "path";
import ejs from "ejs";
import posConfig from "./pos.config.json";
import posGenesis from "./pos.genesis.json";
import poaConfig from "./poa.config.json";
import poaGenesis from "./poa.genesis.json";

function isObject(item: any) {
    return item && typeof item === "object" && !Array.isArray(item);
}

function mergeDeep(target: any, ...sources: any): any {
    if (!sources.length) return target;
    const source = sources.shift();

    if (isObject(target) && isObject(source)) {
        for (const key in source) {
            if (isObject(source[key])) {
                if (!target[key]) Object.assign(target, { [key]: {} });
                mergeDeep(target[key], source[key]);
            } else {
                Object.assign(target, { [key]: source[key] });
            }
        }
    }

    return mergeDeep(target, ...sources);
}

type NodeType = {
    id: string;
    name: string;
    address: string;
    publicKey: string;
};

// 1 - create node ec2 instances and collect their IPs
// 2 - initialize each node locally and collect their id, address and publicKey
// 3 - use a mock node to create genesis file by adding all the nodes addresses and balances and merging the base-genesis.json
// 4 - distribute the genesis file to all nodes
// 5 - sign the genesis transaction in each node
// 6 - collect all the genesis transactions in each node and execute the collect-gentxs
// 7 - distribute the final genesis file and the gentxs to all nodes
// 8 - add the seeds on each node
export class NetworkConfigurator {
    private config: typeof posConfig | typeof poaConfig;
    private baseGenesis: typeof posGenesis | typeof poaGenesis;

    constructor(private consensus: "poa" | "pos", private validatorCount: number, private dataPath = __dirname, private password: string) {
        if (consensus === "poa") {
            this.config = poaConfig;
            this.baseGenesis = poaGenesis;
        } else if (consensus === "pos") {
            this.config = posConfig;
            this.baseGenesis = posGenesis;
        } else {
            throw Error("Consensus not supported");
        }
    }

    exrpd(arg: string, nodeName = "default") {
        return exec(`docker run --rm -v ${path.join(this.dataPath, nodeName)}:/root/.exrpd ${this.config.dockerImage} exrpd ${arg}`, {
            encoding: "utf8",
            stdio: "pipe",
        });
    }

    async configureNode(nodeName: string) {
        await this.exrpd(`config keyring-backend ${this.config.node.keyring}`, nodeName);
        await this.exrpd(`config chain-id ${this.config.network.chain_id}`, nodeName);
        await this.exrpd(
            `keys add ${this.config.node.key_name} --keyring-backend ${this.config.node.keyring} --algo ${this.config.node.key_algo}`,
            nodeName,
        );
        await this.exrpd(`init ${nodeName} --chain-id ${this.config.network.chain_id}`, nodeName);
        return {
            name: nodeName,
            id: await this.exrpd(
                `tendermint show-node-id --chain-id ${this.config.network.chain_id} --keyring-backend ${this.config.node.keyring}`,
                nodeName,
            ).replace(/\n$/, ""),
            address: JSON.parse(await this.exrpd(`keys list --keyring-backend ${this.config.node.keyring} --output json`, nodeName))[0]
                .address,
            publicKey: this.exrpd(`tendermint show-validator`, nodeName).replace(/\n$/, ""),
        };
    }

    async generateGenesis(nodes: NodeType[]) {
        await this.exrpd(`config keyring-backend test`);
        await this.exrpd(`config chain-id ${this.config.network.chain_id}`);
        await this.exrpd(`keys add ${this.config.node.key_name} --keyring-backend test --algo ${this.config.node.key_algo}`);
        await this.exrpd(`init mock-node --chain-id ${this.config.network.chain_id}`);

        // Add validators balance
        const extraAccountsSupply = this.config.network.extra_accounts.reduce((supply, account) => supply + account.balance, 0);
        const validatorsSupply = this.config.network.supply - this.config.network.bridge.initial_balance - extraAccountsSupply;
        for (const nodeInfo of nodes) {
            let nodeAmount = Math.floor(validatorsSupply / nodes.length);
            if (nodeInfo.address === nodes[0].address) {
                // Add the rest if the division is not exact
                nodeAmount += validatorsSupply % nodes.length;
            }
            const parsedNodeAmount = `${nodeAmount}${"0".repeat(18)}`;
            await this.exrpd(
                `add-genesis-account ${nodeInfo.address} ${parsedNodeAmount}${this.config.network.denom} --keyring-backend ${this.config.node.keyring}`,
            );
        }

        // Add bridge account balance
        const bridgeAmount = `${this.config.network.bridge.initial_balance}${"0".repeat(18)}`;
        await this.exrpd(
            `add-genesis-account ${this.config.network.bridge.address} ${bridgeAmount}${this.config.network.denom} --keyring-backend ${this.config.node.keyring}`,
        );

        // Add extra accounts balance
        for (const account of this.config.network.extra_accounts) {
            const accountAmount = `${account.balance}${"0".repeat(18)}`;
            await this.exrpd(
                `add-genesis-account ${account.address} ${accountAmount}${this.config.network.denom} --keyring-backend ${this.config.node.keyring}`,
            );
        }

        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const genesis = require(path.join(this.dataPath, `/default/config/genesis.json`));
        const mergedGenesis = mergeDeep(genesis, this.baseGenesis);
        fs.writeFileSync(path.join(this.dataPath, `/default/config/genesis.json`), JSON.stringify(mergedGenesis));
    }

    async distributeGenesis(nodeName: string) {
        return exec(
            `cp ${path.join(this.dataPath, "/default/config/genesis.json")} ${path.join(this.dataPath, `${nodeName}/config/genesis.json`)}`,
        );
    }

    async signGenesisTransaction(node: NodeType, amount: number) {
        return this.exrpd(
            `--node-id ${node.id} --pubkey "${node.publicKey.replace(/"/g, '\\"')}" gentx ${
                this.config.node.key_name
            } ${amount}${"0".repeat(18)}${this.config.network.denom} --keyring-backend ${this.config.node.keyring} --chain-id ${
                this.config.network.chain_id
            }`,
            node.name,
        );
    }

    async collectGenesisTransactions(nodes: NodeType[]) {
        if (this.consensus !== "poa") {
            await exec(`mkdir ${path.join(this.dataPath, "/default/config/gentx/")}`, { stdio: "pipe" });
            for (const node of nodes) {
                await exec(
                    `cp ${path.join(this.dataPath, `${node.name}/config/gentx/*`)} ${path.join(
                        this.dataPath,
                        "/default/config/gentx/",
                    )} | true`,
                    {
                        stdio: "pipe",
                    },
                );
            }
            await this.exrpd("collect-gentxs");
        } else {
            const defaultGenesis = JSON.parse(fs.readFileSync(path.join(this.dataPath, "/default/config/genesis.json")).toString());
            for (const node of nodes) {
                const nodeGenesis = JSON.parse(fs.readFileSync(path.join(this.dataPath, `/${node.name}/config/genesis.json`)).toString());
                defaultGenesis.app_state.poa.validators.push(...nodeGenesis["app_state"]["poa"]["validators"]);
            }
            fs.writeFileSync(path.join(this.dataPath, "/default/config/genesis.json"), JSON.stringify(defaultGenesis));
        }
    }

    async writeDockerCompose() {
        const template = fs.readFileSync(path.join(__dirname, "/template/docker-compose.yml.ejs")).toString();
        const result = ejs.render(template, { dockerImage: this.config.dockerImage, validatorCount: this.validatorCount });
        fs.writeFileSync(path.join(this.dataPath, `/docker-compose.yml`), result);
    }

    async run() {
        await exec(`rm -rf ${this.dataPath}`);

        // 1 - initialize each node locally and collect their id, address and publicKey
        console.log("1 - Initializing each node locally and collecting their id, address, publicKey and privateKey...");
        const nodePromises = [];
        for (let i = 0; i < this.validatorCount; i++) {
            nodePromises.push(this.configureNode(`validator-${i}`));
        }
        const nodes = await Promise.all(nodePromises);

        // 2 - use a mock node to create genesis file by adding all the nodes addresses and balances and merging the base-genesis.json
        console.log("2 - Generating a mock node to create base genesis file...");
        await this.generateGenesis(nodes);

        // 3 - distribute the genesis file to all nodes
        console.log("3 - Distributing the genesis file to all nodes...");
        for (const node of nodes) {
            await this.distributeGenesis(node.name);
        }

        // 4 - sign the genesis transaction in each node
        console.log("4 - Signing the genesis transactions in each node...");
        const nodeStakeAmount = Math.floor(this.config.network.staked_supply / this.validatorCount);
        for (const node of nodes) {
            await this.signGenesisTransaction(
                node,
                node.address === nodes[0].address
                    ? nodeStakeAmount + (this.config.network.staked_supply % this.validatorCount)
                    : nodeStakeAmount,
            );
        }

        // 5 - collect all the genesis transactions in each node and execute the collect-gentxs
        console.log("5 - Collecting the genesis transactions in each node and execute the collect-gentxs...");
        await this.collectGenesisTransactions(nodes);

        // 3 - distribute the genesis file to all nodes
        console.log("6 - Distributing again the final genesis file to all nodes...");
        for (const node of nodes) {
            await this.distributeGenesis(node.name);
        }
        // seeds = join(",", [for index, id in var.nodes.*.id: "${id}@evm-sidechain-validator-${index}.peersyst.tech:26656"])
        const seeds = nodes.map((node) => `${node.id}@${node.name}:26656`).join(",");
        for (const node of nodes) {
            exec(`sed -i -e 's/seeds = ""/seeds = "${seeds}"/' ${path.join(this.dataPath, node.name, "/config/config.toml")}`);
        }

        await exec(`cp ${path.join(this.dataPath, "/default/config/genesis.json")} ${path.join(this.dataPath, "genesis.json")}`);
        fs.writeFileSync(path.join(this.dataPath, "nodes.json"), JSON.stringify(nodes));
        await exec(`rm -rf ${path.join(this.dataPath, "/default")}`);

        await this.writeDockerCompose();
    }
}
